//
//  ScriptParser.swift
//  Hermes
//
//  Created by FH on 2021/5/10.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

let BundleMainName = "__main"
let BundleActionsName = "__actions"
let BundleSettingsName = "__settings"

func getAppRootPath() -> String {
    return NSHomeDirectory() + "/Library/Caches/Hermes_Mac"
}

struct ScriptParser {
    static var cachedEnumCaseValues: [String : AstNode] = [:]
    let reader: LineReader
    weak var engine: ScriptEngine!
    init(_ scriptPath: String, _ engine: ScriptEngine) {
        self.reader = LineReader(path: scriptPath)!
        self.engine = engine
    }
    
    func getEnumValue(_ valueDesc: String) -> Any? {
        var node: AstNode! = nil
        if let caseNode = Self.cachedEnumCaseValues[valueDesc] {
            node = caseNode
        } else {
            for caseNode in self.engine.getEnumCaseValues() {
                if caseNode.name == valueDesc {
                    Self.cachedEnumCaseValues[valueDesc] = caseNode
                    node = caseNode
                    break
                }
            }
        }
        if node != nil, let itemValue = node.value?.value {
            if node.declType! == "NSInteger" {
                return Int(itemValue)
            } else if node.declType == "NSUInteger" {
                return UInt(itemValue)
            }
        }
        return nil
    }
    
    let scriptTypes: Set = ["default", "scenario", "custom"]
    let sectionNames = ["bundleMain", "bundleActions", "bundleSettings", "bundle",
                        "instrCreateSDK", "instrDestorySDK", "instrJoinChannel", "instrLeaveChannel", "instrName", "instr",
                        "opParams", "opAction", "op"]
    let instrStages = ["createInstance", "configSettings", "startRunning", "exitRunning", "destoryInstance"]
    let directives = [
        // use in instruction
        "breakPoint", "delay", "iterate", "waitCallback", "schedule", "subscribe",
        // use in operator
        "predicate", "waitCallAPI", "preInstance",
        // custom directive only for parse script
        "action", "appHomeDir", "contextToken", "sdkResponds", "sdkNotResponds"
    ]
    let instrDirectives: Set = ["breakPoint", "delay", "iterate", "schedule", "subscribe", "waitCallback", "action"]
    let opDirectives: Set = ["predicate", "waitCallAPI", "contextToken", "preInstance", "appHomeDir", "sdkResponds", "sdkNotResponds"]
    let controlTypes: Set = ["chk", "input", "select", "file", "binary"]
    let compareOpList = ["!=", ">=", ">", "<=", "<"]
    
    enum TermType {
        case action
        case argument
        case comment
        case directive
        case property
        case section
        case symbol
        case value
    }
    
    // for StateMappingType
    enum StateControlType : String {
        case controlType
        case alias
        case range
        case items
        case nonnull
    }
    
    class Term : CustomStringConvertible {
        let type: TermType
        let value: String
        var indent = 0
        var children = [Term]()
        var complexValue: (StateControlType, Any, Any?, Any?)? = nil
        
        var description: String {
            var desc = "\(type) => \(value)"
            if children.count > 0 {
                desc.append(", has children(\(children.count))")
            }
            if complexValue != nil {
                desc.append(", has complexValue")
            }
            return desc
        }
        
        init(type: TermType, value: String) {
            self.type = type
            self.value = value
        }
    }
    
    static func buildTemplate() -> String {
        // write states.keys
        let appContext = HermesContext()
        let mainStates = appContext.mainStates.toDictionary()
        var stringView: String.UnicodeScalarView = "".unicodeScalars
        stringView.append(contentsOf: "; DSL格式说明: \n".unicodeScalars)
        stringView.append(contentsOf: "; settings.States => ".unicodeScalars)
        var handleFirstLine = true
        var hasStates = false
        var lineCount = 1
        for key in mainStates.allKeys.sorted(by: { $0 < $1 }) {
            hasStates = true
            if (handleFirstLine && lineCount % 8 == 0) || (!handleFirstLine && lineCount % 11 == 0) {
                handleFirstLine = false
                lineCount = 1
                stringView.removeLast()
                stringView.append(contentsOf: "\n".unicodeScalars)
                stringView.append(contentsOf: ";                         => ".unicodeScalars)
            }
            stringView.append(contentsOf: key.unicodeScalars)
            stringView.append(contentsOf: "、".unicodeScalars)
            lineCount += 1
        }
        if hasStates {
            stringView.removeLast()
            stringView.append(contentsOf: "\n".unicodeScalars)
        }
        // TODO: remove, update
        stringView.append(contentsOf: "; appConetxt.States => actionEngineRunning、actionEngineStop、actionRespondsScriptAction、gridCount\n".unicodeScalars)
        stringView.append(contentsOf: "\n".unicodeScalars)
        // write default.script content
        if let path = Bundle.main.path(forResource: "default", ofType: "script"), let reader = LineReader(path: path) {
            for line in reader {
                if line.contains("%type: default") {
                    stringView.append(contentsOf: "%type: custom\n".unicodeScalars)
                    stringView.append(contentsOf: "%name: input script's name\n\n".unicodeScalars)
                } else {
                    stringView.append(contentsOf: line.unicodeScalars)
                }
            }
        }
        return String(stringView)
    }
    
    // quick parse script's name and bundleType
    func scan() -> (String, ScriptBundleType) {
        var type = ScriptBundleType.default
        var name = ""
        for line in self.reader {
            let indent = line.indent()
            if indent == 0 && line.starts(with: "%") {
                let split = line.components(separatedBy: " ").map { $0.trimWhiteSpaces() }
                if split.count == 2 {
                    let left = split[0].trim("%", ":")
                    if left == "type" && scriptTypes.contains(split[1]) {
                        type = ScriptBundleType.init(rawValue: split[1])!
                    } else if left == "name" {
                        name = split[1]
                    }
                }
            }
            if name != "" && type != .default {
                break
            }
        }
        if type == .default || name == "" {
            fatalError("parse script error: script's type or name is invalid")
        }
        return (name, type)
    }
    
    func parse() throws -> ScriptBundles {
        // REMARK: lexical analysis
        let queue = Stack<Term>()
        let stack = Stack<Term>()
        var startArgumentsScope = false
        var scriptType: ScriptBundleType = .default
        var scriptName = ""
        var scriptRemark = ""
        loop: for item in self.reader {
            let indent = item.indent()
            var line = item.trimWhiteSpaces()
            if line.count == 0 {
                continue
            }
            // handle comment in expr
            if !line.starts(with: ";") && line.contains(";") {
                let split = line.components(separatedBy: ";")
                line = split[0].trimWhiteSpaces()
            }
            if indent == 0 && line.starts(with: "%") {
                // for properties for script
                if stack.count > 0 || queue.count > 0 {
                    fatalError("parse script error: script's properties must be written at top of file, at \(line)")
                }
                var flag = false
                let split = line.components(separatedBy: " ").map { $0.trimWhiteSpaces() }
                if split.count == 2 {
                    let left = split[0].trim("%", ":")
                    if left == "type" && scriptTypes.contains(split[1]) {
                        scriptType = ScriptBundleType.init(rawValue: split[1])!
                        flag = true
                    } else if left == "name" {
                        scriptName = split[1]
                        flag = true
                    } else if left == "remark" {
                        scriptRemark = split[1]
                        flag = true
                    }
                }
                if !flag {
                    fatalError("parse script error: script's property format is invalid, at \(line)")
                }
            } else if line == "}" {
                startArgumentsScope = false
            } else if line.starts(with: ";;") {
                if let preTerm = stack.top(), preTerm.type == .section, indent <= preTerm.indent {
                    popSections(destIndent: indent, stack)
                }
                let term = Term(type: .comment, value: line.trim(";", " "))
                stack.push(item: term)
            } else if line.starts(with: ";") {
                continue
            } else if line.starts(with: "[") {
                // for mapping control of op's argument or predicate directive
                if let preTerm = stack.top(), preTerm.type == .section && preTerm.value.starts(with: "op"),
                   let node = preTerm.children.last, (node.type == .argument || node.type == .directive) {
                    let content = line.trim("[", "]")
                    let split = content.components(separatedBy: ",")
                    for item in split {
                        let expr = item.trimWhiteSpaces()
                        if expr.starts(with: ".") {
                            // for controlType prop
                            let split = expr.components(separatedBy: "(")
                            let type = split[0].trim(".")
                            if self.controlTypes.contains(type) {
                                var enumType: String? = nil
                                var defaultItem: String? = nil
                                if split.count == 2 {
                                    let subSplit = split[1].components(separatedBy: " ")
                                    for item in subSplit {
                                        if item.starts(with: "=") {
                                            enumType = item.trim("=")
                                        } else {
                                            defaultItem = item.trim(" ", ")", "\"")
                                        }
                                    }
                                }
                                let child = Term(type: .property, value: StateControlType.controlType.rawValue)
                                child.complexValue = (.controlType, type, enumType, defaultItem)
                                node.children.append(child)
                            } else {
                                fatalError("parse script error: mappingControl's type not found, at \(line)")
                            }
                        } else if expr.starts(with: "alias=") {
                            // for alias prop
                            let child = Term(type: .property, value: StateControlType.alias.rawValue)
                            child.complexValue = (.alias, expr.replacingOccurrences(of: "alias=", with: "") , nil, nil)
                            node.children.append(child)
                        } else if expr.contains("..") {
                            // for range prop
                            let child = Term(type: .property, value: StateControlType.range.rawValue)
                            var lower: String = ""
                            var upper: String = ""
                            if expr.starts(with: "...") {
                                upper = expr.replacingOccurrences(of: "...", with: "")
                                if let upperValue = Int(upper) {
                                    let range = ...upperValue
                                    child.complexValue = (.range, range, nil, nil)
                                }
                            } else if expr.starts(with: "..<") {
                                upper = expr.replacingOccurrences(of: "..<", with: "")
                                if let upperValue = Int(upper) {
                                    let range = ..<upperValue
                                    child.complexValue = (.range, range, nil, nil)
                                }
                            } else if expr.hasSuffix("...") {
                                lower = expr.replacingOccurrences(of: "...", with: "")
                                if let lowerValue = Int(lower) {
                                    let range = lowerValue...
                                    child.complexValue = (.range, range, nil, nil)
                                }
                            } else if expr.contains("...") {
                                let split = expr.components(separatedBy: "...")
                                if split.count == 2 {
                                    lower = split[0]
                                    upper = split[1]
                                    if let lowerValue = Int(lower), let upperValue = Int(upper) {
                                        let range = lowerValue...upperValue
                                        child.complexValue = (.range, range, nil, nil)
                                    }
                                }
                            } else if expr.contains("..<") {
                                let split = expr.components(separatedBy: "..<")
                                if split.count == 2 {
                                    lower = split[0]
                                    upper = split[1]
                                    if let lowerValue = Int(lower), let upperValue = Int(upper) {
                                        let range = lowerValue..<upperValue
                                        child.complexValue = (.range, range, nil, nil)
                                    }
                                }
                            }
                            if child.complexValue != nil {
                                node.children.append(child)
                            } else {
                                fatalError("parse script error: mappingControl's range format is invalid, at \(line)")
                            }
                        } else if expr == "nonnull" {
                            let child = Term(type: .property, value: StateControlType.nonnull.rawValue)
                            child.complexValue = (.nonnull, true, nil, nil)
                            node.children.append(child)
                        } else if expr.contains("、") {
                            // for items
                            let items = expr.components(separatedBy: "、").map { $0.trim(" ", "\"") }
                            let child = Term(type: .property, value: StateControlType.items.rawValue)
                            child.complexValue = (.items, items, nil, nil)
                            node.children.append(child)
                        }
                    }
                } else {
                    fatalError("parse script error: mappingControl not find belong op's argument, at \(line)")
                }
            } else {
                if let preTerm = stack.top(), preTerm.type == .section, indent <= preTerm.indent {
                    popSections(destIndent: indent, stack)
                }
                if line.contains(":") && !line.starts(with: ".") {
                    // for section
                    for name in self.sectionNames {
                        if line.starts(with: name) {
                            parseSectionExpr(line, indent, stack, queue)
                            if let top = stack.top(), top.type == .section && top.value.starts(with: "bundle") {
                                var hasName = true
                                if top.value == "bundleSub" {
                                    if top.children.count == 0 || top.children.first!.type != .symbol {
                                        hasName = false
                                    }
                                }
                                if !hasName {
                                    fatalError("parse script error: not found bundle's name, at \(line)")
                                }
                            }
                            continue loop
                        }
                    }
                    // for argument of op section
                    if let preTerm = stack.top(), preTerm.type == .section && preTerm.value.starts(with: "op") {
                        let split = line.components(separatedBy: " ").map { $0.trimWhiteSpaces() }
                        let arg = split[0].trim(":")
                        let child = Term(type: .argument, value: arg)
                        preTerm.children.append(child)
                        // other settings
                        parseOtherSettings(split, parent: child)
                    } else {
                        fatalError("parse script error: argument not find belong op, at \(line)")
                    }
                } else if line.starts(with: ".") {
                    // for directive
                    parseDirectiveExpr(line, parent: stack.top())
                    if line.hasSuffix("{") {
                        startArgumentsScope = true
                    }
                } else if startArgumentsScope {
                    // for arguments
                    if let preTerm = stack.top()?.children.last, preTerm.type == .directive && line.contains("=>") {
                        parseArgumentExpr(line, parent: preTerm)
                    } else {
                        fatalError("parse script error: argument not find belong directive, at \(line)")
                    }
                }
            }
        }
        if stack.count > 0 {
            popSections(destIndent: 0, stack)
        }
        // REMARK: grammatical analysis
        let script = ScriptBundles(type: scriptType, name: scriptName, remark: scriptRemark)
        var currOperator: ScriptOperator! = nil
        var currInstruction: ScriptInstruction! = nil
        var currBundleName: String! = nil
        var currIsMainBundle = false
        var currIsActionsBundle = false
        var currIsSettingsBundle = false
        var hasMainBundle = false
        var hasActionsBundle = false
        var hasSettingsBundle = false
        var hasJoinChannelOp = false
        var hasCreateInstanceOp = false
        var ignoreCurrInstruction = false
        var indexOfInstr = 0
        var replacedActionNames = [String]()
        let saveToBundlesFn: () throws -> Void = {
            // save instr
            if currOperator != nil {
                currInstruction.addOperator(currOperator)
                currOperator = nil
            }
            // save instr
            if currInstruction != nil {
                if !ignoreCurrInstruction {
                    let currIsSubscribeBundle = !(currIsMainBundle || currIsActionsBundle || currIsSettingsBundle)
                    if currInstruction.replacedByActionName.count > 0 && !(currIsMainBundle || currIsSubscribeBundle) {
                        fatalError("parse script error: replaced instr must be used in mainBundle or subscribeBundles.")
                    }
                    if currIsMainBundle {
                        script.addMainInstruction(currInstruction)
                    } else if currIsActionsBundle {
                        script.addActionsInstruction(currInstruction)
                    } else if currIsSettingsBundle {
                        script.addSettingsInstruction(currInstruction)
                    } else {
                        script.addSubscribeInstruction(currInstruction, key: currBundleName)
                    }
                }
                // handle two selector be same where instr in actionBundles and need switch true/false
                if currIsActionsBundle && currInstruction.operators.count == 2,
                   let firstSelector = currInstruction.operators[0].selector,
                   let secondSelector = currInstruction.operators[1].selector, firstSelector == secondSelector {
                    if currInstruction.associatedActionArguments.count == 1 {
                        let actionArguments = currInstruction.associatedActionArguments.allValues[0]
                        if actionArguments.count == 2 {
                            for op in currInstruction.operators {
                                if let opPredicate = op.predicateContextDirective,
                                   case .predicateContext(let k, _, let v, _) = opPredicate,
                                   let opBoolValue = v as? Bool {
                                    for (actionName, predicate) in actionArguments {
                                        if case .predicateContext(let argKey, _, let argValue, _) = predicate,
                                           let boolValue = argValue as? Bool {
                                            if opBoolValue == boolValue && k == argKey {
                                                op.selector = "\(actionName)@\(op.selector!)"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    } else {
                        fatalError("parse script error: \(firstSelector) in bundleActions need .action directive")
                    }
                }
                ignoreCurrInstruction = false
                currInstruction = nil
            }
        }
        
        loop: while let top = queue.dequeue() {
            if top.value.starts(with: "bundle") {
                var node = top
                if node.value == "bundleMain" {
                    node = Term(type: .section, value: "bundle")
                    let child = Term(type: .symbol, value: BundleMainName)
                    node.children.append(child)
                } else if node.value == "bundleActions" {
                    node = Term(type: .section, value: "bundle")
                    let child = Term(type: .symbol, value: BundleActionsName)
                    node.children.append(child)
                } else if node.value == "bundleSettings" {
                    node = Term(type: .section, value: "bundle")
                    let child = Term(type: .symbol, value: BundleSettingsName)
                    node.children.append(child)
                }
                indexOfInstr = 0
                do {
                    try saveToBundlesFn()
                } catch let errMessage {
                    throw errMessage
                }
                var name = ""
                if let symbol = node.children.first, symbol.type == .symbol {
                    name = symbol.value
                }
                currIsMainBundle = name == BundleMainName
                currIsActionsBundle = name == BundleActionsName
                currIsSettingsBundle = name == BundleSettingsName
                if hasMainBundle && currIsMainBundle {
                    fatalError("parse script error: only one main bundle can be defined")
                } else if hasActionsBundle && currIsActionsBundle {
                    fatalError("parse script error: only one actions bundle can be defined")
                } else if hasSettingsBundle && currIsSettingsBundle {
                    fatalError("parse script error: only one settings bundle can be defined")
                } else {
                    currBundleName = name
                    if currIsMainBundle {
                        hasMainBundle = true
                    } else if currIsActionsBundle {
                        hasActionsBundle = true
                    } else if currIsSettingsBundle {
                        hasSettingsBundle = true
                    }
                }
            } else if top.value.starts(with: "instr") {
                var node = top
                let value = node.value.lowercased()
                if value == "instrname" {
                    if let actionName = top.children.first, actionName.type == .symbol {
                        node = Term(type: .section, value: "instr")
                        let child = Term(type: .property, value: "replacedByActionName")
                        let value = Term(type: .symbol, value: actionName.value)
                        child.children.append(value)
                        node.children.append(child)
                    } else {
                        fatalError("parse script error: need to set actionName in script for `instrName`")
                    }
                } else if value == "instrcreatesdk" {
                    node = Term(type: .section, value: "instr")
                    let child = Term(type: .property, value: "createInstance")
                    node.children.append(child)
                    let op = Term(type: .section, value: "op")
                    let action = Term(type: .action, value: "sharedEngineWithAppId:delegate:")
                    op.children.append(action)
                    queue.enqueue(item: op)
                } else if value == "instrjoinchannel" {
                    node = Term(type: .section, value: "instr")
                    let child = Term(type: .property, value: "startRunning")
                    node.children.append(child)
                    let op = Term(type: .section, value: "op")
                    let action = Term(type: .action, value: "joinChannelByToken:channelId:info:uid:joinSuccess:")
                    op.children.append(action)
                    queue.enqueue(item: op)
                } else if value == "instrleavechannel" {
                    node = Term(type: .section, value: "instr")
                    let child = Term(type: .property, value: "exitRunning")
                    node.children.append(child)
                    let op = Term(type: .section, value: "op")
                    let action = Term(type: .action, value: "leaveChannel:")
                    op.children.append(action)
                    queue.enqueue(item: op)
                } else if value == "instrdestorysdk" {
                    node = Term(type: .section, value: "instr")
                    let child = Term(type: .property, value: "destoryInstance")
                    node.children.append(child)
                    let op = Term(type: .section, value: "op")
                    let action = Term(type: .action, value: "destroy")
                    op.children.append(action)
                    queue.enqueue(item: op)
                }
                do {
                    try saveToBundlesFn()
                } catch let errMessage {
                    throw errMessage
                }
                currInstruction = ScriptInstruction(category: .sdk, index: indexOfInstr)
                var hasSubscribeDirective = false
                for item in node.children {
                    if item.type == .comment {
                        currInstruction.setRemark(item.value)
                    } else if item.type == .property {
                        if item.value == "ignore" {
                            ignoreCurrInstruction = true
                            continue loop
                        } else if item.value == "replacedByActionName" {
                            let actionName = item.children[0].value
                            currInstruction.setReplacedActionName(actionName)
                            replacedActionNames.append(actionName)
                        } else if instrStages.contains(item.value) {
                            let stage = ScriptInstructionStage.init(strValue: item.value)!
                            currInstruction.setStage(stage)
                        }
                    } else if item.type == .action {
                        currInstruction.addAction(item.value, nil)
                    } else if item.type == .directive {
                        // handle .action used by script-only
                        if item.value == "action" {
                            var actionName = ""
                            var arguments = [String : ScriptDirective]()
                            for child in item.children {
                                if child.type == .action {
                                    actionName = child.value
                                } else if child.type == .argument,
                                          let stateNode = child.children.first, stateNode.type == .symbol,
                                          let predicateNode = stateNode.children.first, predicateNode.type == .value {
                                    let aliasName = child.value
                                    let state = stateNode.value
                                    let predicateValue = handleStateValue(predicateNode.value)
                                    arguments[aliasName] = .predicateContext(keyPath: state, compareOp: "=",
                                                                             value: predicateValue, control: nil)
                                }
                            }
                            if !actionName.isEmpty && !arguments.isEmpty {
                                currInstruction.addAction(actionName, arguments)
                            }
                        } else {
                            let directive = self.buildDirective(item, instr: currInstruction, op: nil)
                            currInstruction.addDirective(directive)
                            if directive.isKind(of: .subscribeCallback(selector: "", blockor: nil, mapping: [:])) {
                                hasSubscribeDirective = true
                            }
                        }
                    }
                }
                let isSubBundle = !currIsMainBundle && !currIsActionsBundle && !currIsSettingsBundle
                if hasSubscribeDirective {
                    if !isSubBundle {
                        fatalError("parse script error: wrong use of `.subscribeCallback` directive in bundle")
                    }
                } else if indexOfInstr == 0 && isSubBundle {
                    fatalError("parse script error: not found `.subscribeCallback` directive in first instr on sub bundle")
                }
                indexOfInstr += 1
            } else {
                if currOperator != nil {
                    currInstruction.addOperator(currOperator)
                    currOperator = nil
                }
                var selector: String? = nil
                var targetClass: String? = nil
                var forCreateInstance = false
                for item in top.children {
                    if item.type == .action {
                        selector = item.value
                    } else if item.type == .value {
                        targetClass = item.value
                    } else if item.type == .property {
                        forCreateInstance = item.value == "init"
                    }
                }
                var node = top
                if top.value.lowercased() == "opparams" {
                    node = Term(type: .section, value: "op")
                    selector = "setParameters:"
                    // handle json value
                    var json = "{"
                    for item in top.children {
                        if item.type == .argument {
                            for valueItem in item.children {
                                if valueItem.type == .value {
                                    json.append("\(item.value): \(valueItem.value)")
                                    break
                                }
                            }
                        } else if item.type == .directive {
                            node.children.append(item)
                        }
                    }
                    json.append("}")
                    let arg = Term(type: .argument, value: "options")
                    arg.children.append(Term(type: .value, value: json))
                    node.children.append(arg)
                } else if top.value.lowercased() == "opaction" {
                    node = Term(type: .section, value: "op")
                    var actionName = ""
                    for item in top.children {
                        if item.type == .symbol {
                            actionName = item.value
                        } else if item.type == .argument {
                            node.children.append(item)
                        } else if item.type == .directive {
                            node.children.append(item)
                        }
                    }
                    if actionName != "" {
                        let directive = Term(type: .directive, value: "contextCallAction")
                        let arg = Term(type: .argument, value: actionName)
                        directive.children.append(arg)
                        node.children.append(directive)
                    } else {
                        fatalError("parse script error: need to set `appContext.actionXXX` for op before set opAction")
                    }
                }
                if selector.isEmpty {
                    if targetClass.isEmpty {
                        currOperator = ScriptOperator()
                        hasCreateInstanceOp = false
                    } else {
                        if !forCreateInstance {
                            fatalError("parse script error: only set targetClass is invalid.")
                        }
                        if !hasCreateInstanceOp {
                            selector = "init"
                        } else {
                            fatalError("parse script error: pre operator's instance not be used.")
                        }
                    }
                }
                if let selector = selector {
                    // handle group
                    var group: String? = nil
                    if selector.starts(with: "sharedEngine") {
                        group = "sharedEngine"
                    } else if selector.starts(with: "joinChannel") {
                        group = "joinChannel"
                        hasJoinChannelOp = true
                    } else if selector.starts(with: "leaveChannel") {
                        group = "leaveChannel"
                    }
                    currOperator = ScriptOperator(selector: selector, group: group,
                                                  targetClass: targetClass, forCreateInstance: forCreateInstance)
                }
                // handle arguments & directives
                var waitCallAPIDirective: ScriptDirective? = nil
                var contextCallActionDirective: ScriptDirective? = nil
                var predicateDirective: ScriptDirective? = nil
                var predicateResponds: ScriptDirective? = nil
                for item in node.children {
                    if item.type == .argument {
                        // handle state arg
                        let (arg, state, value, _) = handleArgumentState(item)
                        var argName = arg
                        // parse $0、$1 => param’s name
                        if arg.starts(with: "$") {
                            method: for node in self.engine.getAPIsMethods() where node.name == selector! {
                                var index = 0
                                for param in node.children where param.type == .parameter {
                                    let argIndex = Int(arg.trim("$"))!
                                    if argIndex == index {
                                        argName = param.name
                                        break method
                                    }
                                    index += 1
                                }
                            }
                        }
                        // handle directive arg
                        if item.children.count == 1, let child = item.children.first, child.type == .directive {
                            if child.value.lowercased() == "contextToken".lowercased() {
                                currOperator.addDirectiveArg(argName, ScriptDirective.contextArgToken)
                            } else if child.value.lowercased() == "preInstance".lowercased() {
                                currOperator.addDirectiveArg(argName, ScriptDirective.preOperatorInstance)
                                if !hasCreateInstanceOp {
                                    fatalError("parse script error: pre operator's instance not be found")
                                } else {
                                    hasCreateInstanceOp = false
                                }
                            } else if child.value.lowercased() == "appHomeDir".lowercased(),
                                      let file = child.children.first, file.type == .value {
                                let path = NSHomeDirectory() + "/Library/Caches/Hermes_Mac" + file.value.trimString()
                                currOperator.addArgValue(argName, path)
                            }
                            continue
                        }
                        if state == "" {
                            currOperator.addArgValue(argName, value)
                        } else {
                            let control = handleMappingControl(item)
                            let stateDirective = ScriptDirective.contextStates(keyPath: state, initValue: value, control: control)
                            currOperator.addDirectiveArg(argName, stateDirective)
                        }
                    } else if item.type == .directive {
                        let value = item.value.lowercased()
                        let directive = self.buildDirective(item, instr: nil, op: currOperator)
                        if predicateDirective == nil && value == "predicate" {
                            predicateDirective = directive
                        } else if predicateResponds == nil && (value == "sdkresponds" || value == "sdknotresponds") {
                            predicateResponds = directive
                        } else if contextCallActionDirective == nil && value == "contextcallaction" {
                            contextCallActionDirective = directive
                        } else if waitCallAPIDirective == nil {
                            waitCallAPIDirective = directive
                        }
                    }
                }
                if hasCreateInstanceOp {
                    fatalError("parse script error: not found .preInstance directive.")
                }
                hasCreateInstanceOp = forCreateInstance
                currOperator.addCallActionDirectives(waitCallAPIDirective, contextCallActionDirective)
                currOperator.addPredicateDirectives(predicateDirective, predicateResponds)
            }
        }
        do {
            try saveToBundlesFn()
        } catch let errMessage {
            throw errMessage
        }
        if !hasMainBundle {
            fatalError("parse script error: not found main bundle")
        }
        if !hasJoinChannelOp {
            fatalError("parse script error: not found joinChannel op")
        }
        for name in replacedActionNames {
            if !script.checkReplacedActionName(name) {
                fatalError("parse script error: not found replaced actionName: \(name) in script file")
            }
        }
        return script
    }
    
    // REMARK: help methods for grammatical
    
    private func buildDirective(_ item: Term, instr: ScriptInstruction?, op: ScriptOperator?) -> ScriptDirective {
        let value = item.value.lowercased()
        let children = item.children
        if instr != nil {
            if value == "breakpoint" {
                return .breakPoint
            } else if value == "schedule" {
                if children.count == 1 {
                    return .schedule(runInMainThread: children[0].value == "main")
                } else {
                    return .schedule(runInMainThread: false)
                }
            } else if value == "delay" {
                if children.count == 1, let value = TimeInterval(children[0].value) {
                    return .delay(interval: value)
                }
            } else if value == "iterate" {
                if children.count == 1, let value = Int(children[0].value) {
                    return .iterate(times: value)
                }
            } else if value == "waitcallback" || value == "subscribe" {
                if children.count > 0 {
                    let mapping: OrderedDictionary<String, String> = [:]
                    var action: String! = nil
                    var block: String? = nil
                    var directiveArg: String? = nil
                    for item in children {
                        if item.type == .action {
                            action = item.value
                            if item.children.count == 1 {
                                block = item.children[0].value
                            }
                        } else if item.type == .argument && item.children.count > 0 {
                            let (key, value) = handleWaitCallDirectiveArgs(item)
                            mapping[key] = value
                        } else if item.type == .property {
                            directiveArg = item.value
                        }
                    }
                    // handle cell mapping arg
                    if let arg = directiveArg {
                        mapping.insertValue(getCellMappingStreamId(arg, 0), forKey: "__ignoreKey", at: 0)
                    }
                    for (key, _) in mapping {
                        if (key.contains("cell.") || key.contains("cells.")) && directiveArg == nil {
                            fatalError("parse script error: access cell.XXX need set mapping arg first.")
                        }
                    }
                    
                    if action != nil {
                        if value == "waitcallback" {
                            return .waitCallback(selector: action, blockor: block, mapping: mapping)
                        } else {
                            return .subscribeCallback(selector: action, blockor: block, mapping: mapping)
                        }
                    } else {
                        var tip = " please see: "
                        for item in children {
                            if item.type == .symbol {
                                tip += item.value
                                break
                            }
                        }
                        fatalError("parse script error: .\(item.value)'s actionName is available.\(tip)")
                    }
                }
            }
        } else if let op = op {
            if value == "waitcallapi" {
                if children.count > 0 && op.forCallAPIs {
                    let mapping: OrderedDictionary<String, String> = [:]
                    for item in children {
                        if item.type == .argument {
                            let (key, value) = handleWaitCallDirectiveArgs(item)
                            mapping[key] = value
                        }
                    }
                    return .waitCallAPI(mapping: mapping)
                } else {
                    fatalError("parse script error: need to set `selector` for op before set .waitCallAPI")
                }
            } else if value == "contextcallaction" {
                if let actionName = item.children.first, actionName.type == .argument {
                    if actionName.value.contains("action") || actionName.value.hasSuffix("Trigger") {
                        return ScriptDirective.contextCallAction(methodName: actionName.value, args: [:])
                    } else {
                        fatalError("parse script error: methodName must be start with `action` or end with `trigger`")
                    }
                } else {
                    fatalError("parse script error: not found `actionName` for .contextCallAction")
                }
            } else if value == "predicate" {
                let (_, state, value, compareOp) = handleArgumentState(item)
                let control = handleMappingControl(item)
                return ScriptDirective.predicateContext(keyPath: state, compareOp: compareOp, value: value, control: control)
            } else if value == "sdkresponds" || value == "sdknotresponds" {
                let compareOp = value == "sdknotresponds" ? "!=" : "="
                let className = op.targetClass ?? ""
                var methodName = ""
                if children.count > 0, let actionNode = children.first, actionNode.type == .action {
                    methodName = actionNode.value
                } else if let selector = op.selector {
                    methodName = selector
                } else {
                    fatalError("parse script error: need `selector` arg to set .predicateRespond")
                }
                return ScriptDirective.predicateResponds(methodName: methodName, className: className, compareOp: compareOp)
            }
        }
        return .idle
    }
    
    private func handleArgumentState(_ arg: Term) -> (String, String, Any?, String) {
        let name = arg.value
        var state: String = ""
        var value: Any? = nil
        var compareOp = "="
        if let node = arg.children.first {
            if node.type == .value {
                value = handleStateValue(node.value)
            } else {
                state = node.value
                if let valueNode = node.children.first {
                    let valueStr = valueNode.value
                    // handle init case-value, like $(stateName=.3)
                    if valueStr.starts(with: ".") && String(valueStr.dropFirst()).isNumber() {
                        value = "__Case: \(String(valueStr.dropFirst()))"
                    } else {
                        value = handleStateValue(valueStr)
                        if node.children.count == 2, let compareNode = node.children.last, compareNode.type == .property {
                            compareOp = compareNode.value
                        }
                    }
                }
            }
        }
        return (name, state, value, compareOp)
    }
    
    private func handleStateValue(_ value: String) -> Any? {
        if value == "nil" {
            return nil
        } else if value == "()" {
            return ()
        } else if let result = value.checkBoolean() {
            return result
        } else if let result = value.checkNumber() {
            return result
        } else if let result = value.checkInputString() {
            return result
        } else if value.starts(with: "Agora") {
            return self.getEnumValue(value) ?? value
        } else {
            return value
        }
    }
    
    private func handleWaitCallDirectiveArgs(_ arg: Term) -> (String, String) {
        let key = arg.children[0].value
        var value = arg.value
        if value == "void" {
            value = "__void"
        } else if let result = value.checkBoolean() {
            value = getCustomStateValue(result)
        } else if let result = value.checkNumber() {
            value = getCustomStateValue(result)
        } else if let result = value.checkInputString() {
            value = getCustomStateValue(result)
        } else if value.starts(with: "Agora"), let result = self.getEnumValue(value) {
            value = getCustomStateValue(result)
        }
        return (key, value)
    }
    
    private func handleMappingControl(_ node: Term) -> StateMappingControl? {
        var controlType: StateMappingControlType = .none
        var alias: String = ""
        var enumClass: String? = nil
        var defaultSelectItem: String? = nil
        var range: IntRange? = nil
        var nonnull = false
        var items: [String]? = nil
        for item in node.children {
            if item.type == .property, let tuple = item.complexValue {
                if tuple.0 == .controlType, let type = tuple.1 as? String {
                    if type == "chk" {
                        controlType = .checkbox
                    } else if type == "input" {
                        controlType = .inputField
                    } else if type == "select" {
                        controlType = .dropbox
                    } else if type == "file" {
                        controlType = .inputFile
                    } else if type == "binary" {
                        controlType = .inputBinary
                    }
                    if let enumName = tuple.2 as? String {
                        enumClass = enumName.trim(")")
                    }
                    if let defaultItem = tuple.3 as? String {
                        defaultSelectItem = defaultItem
                    }
                } else if tuple.0 == .alias, let name = tuple.1 as? String {
                    alias = name
                } else if tuple.0 == .range, let intRange = tuple.1 as? IntRange {
                    range = intRange
                } else if tuple.0 == .nonnull, let flag = tuple.1 as? Bool {
                    nonnull = flag
                } else if tuple.0 == .items, let strItems = tuple.1 as? [String] {
                    items = strItems
                }
            }
        }
        if controlType == .none {
            return nil
        } else {
            let control = StateMappingControl(controlType, alias,
                                              enumClass: enumClass, defaultSelectItem: defaultSelectItem,
                                              intRange: range, stringItems: items)
            control.nonnull = nonnull
            return control
        }
    }
    
    // REMARK: help methods for lexical
    
    private func popSections(destIndent: Int, _ stack: Stack<Term>) {
        while let top = stack.top() {
            if top.type == .section && destIndent <= top.indent {
                stack.pop()
            } else {
                break
            }
        }
    }
    
    private func parseSectionExpr(_ expr: String, _ indent: Int, _ stack: Stack<Term>, _ queue: Stack<Term>) {
        let split = expr.components(separatedBy: " ").map { $0.trimWhiteSpaces() }
        let name = split[0]
        var term: Term!
        if name.hasSuffix(":") {
            term = Term(type: .section, value: name.trim(":", " "))
            term.indent = indent
            if let preTerm = stack.top(), preTerm.type == .comment {
                let comment = stack.pop()!
                if term.value == "instr" {
                    term.children.append(comment)
                }
            }
            stack.push(item: term)
            queue.push(item: term)
        } else {
            fatalError("parse script error: not found name, at \(expr)")
        }
        // other settings
        parseOtherSettings(split, parent: term)
    }
    
    private func parseDirectiveExpr(_ expr: String, parent: Term?) {
        let split = expr.components(separatedBy: " ").map { $0.trimWhiteSpaces() }
        let directive = split[0].trim(".")
        let hasDirectiveArg = directive.contains("(") && directive.contains(")")
        var child: Term!
        for name in self.directives {
            if directive.lowercased().starts(with: name.lowercased())
                && (directive.count == name.count
                    || directive.hasSuffix(":")
                    || hasDirectiveArg) {
                child = Term(type: .directive, value: name)
                if hasDirectiveArg {
                    let regex = try! Regex(pattern: "\(name)\\((.+?)\\).*", groupNames: ["arg"])
                    if let match = regex.findFirst(in: directive), let value = match.group(named: "arg") {
                        let argTerm = Term(type: .property, value: value)
                        child.children.append(argTerm)
                    }
                }
                // check belong
                if let preTerm = parent, preTerm.type == .section
                    && ((preTerm.value == "instr" && instrDirectives.contains(name))
                        || ((preTerm.value == "op" || preTerm.value == "opParams" || preTerm.value == "opAction")
                            && opDirectives.contains(name))) {
                    preTerm.children.append(child)
                } else {
                    fatalError("parse script error: directive not find belong section, at \(expr)")
                }
                break
            }
        }
        // other settings
        parseOtherSettings(split, parent: child)
    }
    
    private func parseStateExpr(_ expr: String) -> Term {
        let content = expr.trim("$", "(", ")")
        var split = content.components(separatedBy: "=")
        var compareOp = "="
        for op in compareOpList {
            let tmp = content.components(separatedBy: op)
            if tmp.count == 2 {
                split = tmp
                compareOp = op
                break
            }
        }
        let keyPath = split[0]
        let child = Term(type: .symbol, value: keyPath)
        if split.count == 2 {
            let defaultValue = split[1]
            let value = Term(type: .value, value: defaultValue)
            child.children.append(value)
            if compareOp != "=" {
                let compareNode = Term(type: .property, value: compareOp)
                child.children.append(compareNode)
            }
        }
        return child
    }
    
    private func parseArgumentExpr(_ expr: String, parent: Term) {
        let split = expr.components(separatedBy: "=>").map { $0.trim(" ", ",") }
        if split.count == 2 {
            let arg = split[0]
            let state = split[1]
            let child = Term(type: .argument, value: arg)
            let stateTerm = parseStateExpr(state)
            child.children.append(stateTerm)
            parent.children.append(child)
        }
    }
    
    private func parseOtherSettings(_ items: [String], parent: Term) {
        for i in 1..<items.count {
            let item = items[i]
            if item.starts(with: "$(") && item.hasSuffix(")") {
                // handle mapping context's state
                let child = parseStateExpr(item)
                parent.children.append(child)
            } else if item.starts(with: ".") {
                // handle arg directive
                let split = item.components(separatedBy: "(")
                let name = split[0].trim(".")
                var value = ""
                if split.count == 2 {
                    value = split[1].trim(")")
                }
                let child = Term(type: .directive, value: name)
                if value.count > 0 {
                    let valueNode = Term(type: .value, value: value)
                    child.children.append(valueNode)
                }
                parent.children.append(child)
            } else if item.starts(with: "%") {
                // handle property
                let child = Term(type: .property, value: item.trim("%"))
                if parent.type == .section && parent.value == "instr"
                    && !(child.value == "ignore" || self.instrStages.contains(child.value)) {
                    fatalError("parse script error: instr's properties is invaild, at \(item)")
                }
                parent.children.append(child)
            } else if item.starts(with: "@") {
                // handle targetClass
                let child = Term(type: .value, value: item.trim("@"))
                parent.children.append(child)
            } else if item.starts(with: "#") {
                // handle action
                var blockor: String? = nil
                var selector = item.trim("#")
                if selector.contains("(") && selector.hasSuffix(")") {
                    let split = selector.components(separatedBy: "(")
                    selector = split[0]
                    blockor = split[1].trim(")")
                }
                let child = Term(type: (parent.value == "instrName" ? .symbol : .action), value: selector)
                if let value = blockor {
                    let blockerTerm = Term(type: .action, value: value)
                    child.children.append(blockerTerm)
                }
                parent.children.append(child)
            } else if item.count > 0 && item != "{" {
                // handle symbol
                let child = Term(type: (parent.type == .argument) ? .value : .symbol, value: item)
                parent.children.append(child)
            }
        }
    }
}
